#!/bin/bash

_usage() {
if [ -t 0 ]; then [[ "$*" != "" ]] && echo "$*
"
cat <<EOF
Openbox pipemenu to create a list of recent files with or without icons.

Usage: $0 [options]

Options:
-m int  maximum number of results to display. Default: $max
-s str  Consider icon sizes between min-max. Default: $isizes
-n      No icons at all. Default is to use icons.
-f      No full path on menu label, only filename
-x str  Application to mime-open files. xdg-open has some pitfalls when no DE
        is defined, mimeo (xyne.archlinux.ca/projects/mimeo/) might be more
        robust. Default: $mimeopener
-r str  Recentfile to parse. Default: ${recentfile/$HOME/\~}
-i str  Full path to mimetype icon directory to store symlinks to exisiting
        icons. Default: $icondir
-g str  GTK rc/ini file to extract icon theme from.
        Default: ${gtksettingsfile/$HOME/\~}
-c      Clear the recentfile (access from pipemenu).

-d      Output debug info to stderr
-O str str  Attempt to find an icon for the supplied mimetype and category,
            then exit.

Dependencies:
        xmlstarlet: parsing recentfile
Optional:
        gio (glib2, libglib2.0-bin): get icons
EOF
else
echo "<openbox_pipe_menu>"
[[ "$*" != "" ]] && echo "<item label=\"$*\"/>"
_endmenu
fi
exit 1
}
_debug() { :; } # redefine this func when debug is enabled
_G() {
    RETVAL=""
    # simplistic grep replacement
    # only works on files, only the first match is assigned to a variable
    # uses regex (what kind of regex?!)
    # $1: regex
    # $2: file
    while read -r line; do
        [[ "$line" =~ $1 ]] && {
            RETVAL="$line"; _debug ">>--- G: found $1 in $2"; return; }
    done <"$2"
    return 1
}
_getdirs() {
    # get all relevant (see isizes) icon directories from one theme and append
    # to the icondirlist array.
    # $1: icontheme's index.theme
    _debug "####################### _getdirs: Getting and filtering Directories for $1 #######################"
    _G "^Directories=" "$1"
    local dirs="${RETVAL#*=}" # remove leading Directories=
    dirs="${dirs%,}" # Remove a possible trailing comma
    oldifs="$IFS"; IFS=, # temporarily change IFS to comma
    dirs=($dirs)
    IFS="$oldifs"
    _debug "## All the subdirectories of ${1%*/}: ${dirs[*]} #######################"
    # Filer out what we want: $mimequery, $isizes
    smallest="${isizes%-*}"
    largest="${isizes#*-}"
    basedir="${1%/*}" # always prepend base directory to icon dir
    for query in "${mimequery[@]}"; do
        for dir in "${dirs[@]}"; do
            _debug "### $basedir/$dir "
            if [[ "$dir" == *"$query"* ]]; then
                # isolate subdir:
                size="${dir//$query/}"; size="${size//\//}"
                [[ "$size" == *'@'* ]] && continue # we don't want these at all
                if [[ "$size" == scalable ]]; then
                    _debug ">=>=>=>=>=> Found scalable in $dir, adding it to icondirlist."
                    icondirlist[$((I++))]="$basedir/$dir"
                else
                    # isolate only 1st number by removing all letters that follow:
                    size="${size%%[a-zA-Z-_]*}"
                    _debug "Size is $size - largest is $largest, smallest is $smallest"
                    # Is this number between smallest and largest? Then add it to icondirlist
                    (( size <= largest && size >= smallest )) && { _debug ">=>=>=>=>=> Found $size in $dir, adding it to icondirlist."; icondirlist[$((I++))]="$basedir/$dir"; }
                fi
            fi
        done
    done
}
_loaddirlist() {
    _debug "_useicons: Compile icon search paths"
    if [ -r "$icondirlist_file" ]; then
        _debug "Found $icondirlist_file - no need to compile it."
        mapfile -t icondirlist <"$icondirlist_file"
    else
    # specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
    # Each theme is stored as subdirectories of the base directories.
    # The internal name of the theme is the name of the subdirectory, although the user-visible name as specified by the theme may be different.
    # Hence, theme names are case sensitive, and are limited to ASCII characters. Theme names may also not contain comma or space. 
        mkdir -p "$icondir" || _usage "Option -${opt}: Cannot create directory $icondir"
        iconthemepath=""
        iconthemeinherits=""
        allinherits=()
        themepath=""
        a=0
        _themepath() {
            for path in "${XDG_DATA_HOME-"$HOME/.local/share"}/icons" "/usr/share/icons"; do
                path="$path/$1/index.theme"
                [ -r "$path" ] && { _debug "_themepath: found $path"; RETVAL="$path"; return 0; }
            done
            RETVAL=""; _debug "_themepath: no path found for $1"; return 1
        }
        _inherits() {
            # this function is calling itself until it finds no more Inherits
            # $1: themepath index.theme
            _G '^Inherits=' "$1" || { _debug "No Inherits found"; return 1; }
            iconthemeinherits="${RETVAL#*=}"
            oldifs="$IFS"; IFS=, # temporarily change IFS to comma
            iconthemeinherits=($iconthemeinherits)
            IFS="$oldifs"
            ### now extract dirlist for each inherit and continue filling icondirlist array and icondirlist_file
            for theme in "${iconthemeinherits[@]}"; do
                for inh in "${allinherits[@]}"; do
                    [[ "$inh" == "$theme" ]] && continue 2
                    allinherits[a++]="$theme"
                done
                _debug "Inherit: $theme"
                if _themepath "$theme"; then
                    themepath="$RETVAL"
                    _getdirs "$themepath"; _inherits "$themepath"
                fi
            done
            return 0
        }
        _debug "Getting current icon theme & inherits."
        _G gtk-icon-theme-name "$gtksettingsfile" || { useicons=0; _debug "no icon theme found"; return 1; }
        icontheme="${RETVAL##*=}" # the internal name of the theme
        icontheme="${icontheme#*\"}"
        icontheme="${icontheme%\"*}"
        allinherits[a++]="$icontheme"
        _themepath "$icontheme" || { _debug "no path found for icontheme \"$icontheme\""; return 1; }
        themepath="$RETVAL"
        _getdirs "$themepath"; _inherits "$themepath"
        # icondirlist is now compiled; write it out to icondirlist_file
        printf "%s\n" "${icondirlist[@]}" > "$icondirlist_file"
    fi
}
_useicons() {
    _loaddirlist || return 1
    ###########################################################################
    _debug "==== Collecting icon choices for ${#array_file[@]} files ===="
    # to get only the last line of the gio output for each file we use mapfile with
    # a callback function, see: wiki.bash-hackers.org/commands/builtin/mapfile#the_callback
    mtf() {
        # $1: index of what mapfile is processing right now
        # $2: string contained in that index
        local string="${2##*:}" # remove "standard::icon" from front
        string="${string// /}" # remove spaces => nice comma-separated list of icons
        array_icon[$(($1/5))]="$string"
        _debug "mapfile index $1: ${array_file[$(($1/5))]}: $string"
    }
    mapfile -t -c 5 -C 'mtf ' <<<"$(gio info -a standard::icon "${array_file[@]#*$sep}")"
}
_geticon() {
    # gets the icon from a dir from the icondirlist
    # $1: comma-separated list of icon names
    # #not used anymore $2: category of icon: [mime|apps|actions|places]
    RETVAL=""
    for list in "$1" "$unknownicon"; do
        _debug "_geticon: searching for $list in $2 dirs"
        oldifs="$IFS"; IFS=, # temporarily change IFS to comma
        names=($list) # convert to array
        IFS="$oldifs"
        _debug "names: ${names[*]}"
        # first, let's see if we have it already
        for name in "${names[@]}"; do
            for found in "$icondir/$name".???; do
                [ -r "$found" ] && { RETVAL="$found"; _debug "Found it already in icondirlist: $found"; return; }
            done
        done
        # search each dir in icondirlist
        for name in "${names[@]}"; do
            for dir in "${icondirlist[@]}"; do
                #~ # skip if it's not the desired category (mime/actions/places/apps)
                #~ [[ "$dir" != *"$2"* ]] && continue
                for found in "$dir/$name".???; do
                    _debug "Searching for $found ..."
                    if [ -r "$found" ]; then
                        _debug "Found it! And return."
                        ext="${found##*.}"
                        # found! make a symlink...
                        ln -s "$found" "$icondir/$name.$ext" >&2
                        # ... and return that
                        RETVAL="$icondir/$name.$ext"
                        return 0
                    fi
                done
            done
        done
    done
    return 1
}
_endmenu() {
    if ((allentries>0)); then
        echo -n "<separator/><item"
        [[ "$useicons" == 1 ]] && _geticon "$deleteicon" && echo -n " icon=\"$RETVAL\""
        echo -n " label=\"Clear recents… ($allentries entries"
        ((unreadable>0)) && echo -n " - $unreadable unreadable"
        echo ")\">
            <action name=\"Execute\"><prompt>Really delete ${recentfile//$HOME/\~}?</prompt><command><![CDATA[\"$0\" -c]]></command></action>
        </item>"
    fi
    if [ -d "$icondir" ]; then
        echo -n "<item label=\"Clear icon cache…\" "
        [[ "$useicons" == 1 ]] && _geticon "$deleteicon" && echo -n " icon=\"$RETVAL\""
        echo ">
            <action name=\"Execute\"><prompt>Really delete ${icondir//$HOME/\~}?</prompt><command><![CDATA[rm -r \"$icondir\"]]></command></action>
        </item>"
    fi
    echo "</openbox_pipe_menu>"
}
_decode() {
    # unescape from XML
    RETVAL="${1//&amp;/&}"
    RETVAL="${RETVAL//&apos;/\'}"
    RETVAL="${RETVAL//&quot;/\"}"
    # decode url encoding - unix.stackexchange.com/a/187256
    LC_ALL=C printf -v RETVAL "%b" "${RETVAL//%/\\x}"
}
#\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
#||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#//////////////////////////////////////////////////////////////////////////////
max=20 # maximum number of recent files
recentfile="${XDG_DATA_HOME-"$HOME/.local/share"}/recently-used.xbel"
useicons=1
fullpath=1
mimeopener="xdg-open"
dep=( xmlstarlet )
sep='|'
only_get_icon=0
gtksettingsfile="${XDG_CONFIG_HOME-"$HOME/.config"}/gtk-3.0/settings.ini"
icondir="${XDG_CONFIG_HOME-"$HOME/.config"}/openbox/mimetype_icons"
isizes=22-64 # exactly two minus-separated icon sizes: smallest-largest
execute_and_update=0 # execute and update
allentries=0 # counting all entries in recentfile...
unreadable=0 # ... of which some are unreadable

# initialise a global RETVAL to be used in functions - avoids opening a subshell
# https://rus.har.mn/blog/2010-07-05/subshells/
RETVAL=''

while getopts "dcs:m:r:g:i:nfx:XOh" opt; do
    case $opt in
        d)  _debug() {
                echo "[31m${*}(B[m" >&2
                return 0
            }
        ;;
        c)
            cat <<EOF > "$recentfile"
<?xml version="1.0" encoding="UTF-8"?>
<xbel version="1.0"
      xmlns:bookmark="http://www.freedesktop.org/standards/desktop-bookmarks"
      xmlns:mime="http://www.freedesktop.org/standards/shared-mime-info"
>
</xbel>
EOF
        exit
        ;;
        s) isizes="$OPTARG"
        ;;
        m)  [[ "$OPTARG" =~ [0-9]+ ]] && (( OPTARG >= 0 )) && (( OPTARG <= 65535 )) || _usage "Option -${opt}: invalid number $OPTARG"
            max="$OPTARG"
        ;;
        r)  [ -r "$OPTARG" ] || _usage "Option -${opt}: Cannot read $OPTARG"
            recentfile="$OPTARG"
        ;;
        g)  [ -r "$OPTARG" ] || _usage "Option -${opt}: Cannot read $OPTARG"
            gtksettingsfile="$OPTARG"
        ;;
        i)  [[ "$OPTARG" == /* ]] || _usage "Option -${opt}: Invalid path $OPTARG - must be absolute"
            icondir="$OPTARG"
        ;;
        n)  useicons=0
        ;;
        f)  fullpath=0
        ;;
        x)  if [ -n "$OPTARG" ]; then
                type "$OPTARG" >/dev/null 2>&1 || _usage "Option -${opt}: $OPTARG is not in PATH"
                mimeopener="$OPTARG"
            else
                mimeopener=""
            fi
        ;;
        X)  # execute command & update recentfile
            execute_and_update=1
        ;;
        O)  # only get an icon
            only_get_icon=1 
        ;;
        h)  _usage
        ;;
        *)  _usage "Invalid option -$OPTARG"
        ;;
    esac
done
shift $((OPTIND-1))

# called by the pipemenu itself to execute commands and simultaneously update $recentfile
if [[ "$execute_and_update" == 1 ]]; then
    [ -t 0 ] && echo "This option should only be called by the pipemenu itself." && exit 1
    item="$1"; shift
    recentfile="$1"; shift
    "$@" & # executing command
    # Now updating recently-used file
    # date format: xbel.sourceforge.net/language/versions/1.0/xbel-1.0.xhtml and www.w3.org/TR/NOTE-datetime
    TZ=UTC printf -v time "%(%FT%R:%S.499999Z)T" # bash doesn't deal with fractions of seconds, they have been replaced with 499999
    tmp="$(xmlstarlet ed -u "/xbel/bookmark[@href=\"$item\"]/@visited" -v "$time" "$recentfile")"
    [[ "$tmp" != "" ]] && echo "$tmp" > "$recentfile"
    exit
fi

# a few things that are required only when you use icons:
if [[ "$useicons" == 1 ]]; then
    icondirlist_file="$icondir/icondirlist"
    icondirlist=() # global list of icn directories to search for mimetypes
    I=0 # global counter for icondirlist
    mimequery=( mimes mimetypes apps places )
    deleteicon="user-trash,user-trash-symbolic"
    unknownicon="unknown,stock_unknown,gnome-unknown"
    [[ "$only_get_icon" == 1 ]] && { _loaddirlist; echo "$1"; echo "$2"; _geticon "$1"; echo "$RETVAL"; exit; }
fi

# quick dependency check
type xmlstarlet >/dev/null || usage "Dependency missing: $x"

# Use an Xpath expression to collect all
# 1) file:///...
# 2) application
# elements, sorted by last modified date. Read into array and limit to $max elements.
mapfile -t array_raw <<<"$(xmlstarlet sel -t -m "/xbel/bookmark" -s D:T:U '@visited' -n -v '@href' -o "$sep" -m 'info/metadata/bookmark:applications/bookmark:application' -s D:T:U '@modified' -v '@exec' -o "$sep" "$recentfile")"
[[ "${array_raw[*]}" == "" ]] && _usage "No recent files."
allentries="$(( ${#array_raw[@]} - 1))"

[ -z "$mimeopener" ] || type "$mimeopener" >/dev/null 2>&1 || mimeopener="" >&2

array_file=()
array_cmd=()
# start counting at 1 because the first array element is always an empty line due to the xmlstarlet command
for ((i=1,j=0;i<${#array_raw[@]};i++)); do
    (( j >= max )) && break
    _debug "Raw line:   ${array_raw[i]}"
    oldifs="$IFS"; IFS="$sep"
    line=(${array_raw[i]})
    IFS="$oldifs"
    # filepath - remove file:// and quotes, then decode both XML escapes and URL encoding
    _decode "${line[0]#*file://}"
    file="$RETVAL"
    # drop unreadable files here
    [ -r "$file" ] || { _debug "Unreadable: $file"; ((unreadable++)); continue; }
    ##########################
    _debug "Readable:   ${line[0]}${sep}$file"
    array_file[j]="${line[0]}${sep}$file"
    for ((k=1;k<${#line[@]};k++)); do
        cmd="${line[k]}"
        # command to open file
        # remove single quotes, spaces and %u
        cmd="${cmd#*\'}"
        cmd="${cmd% %u\'}"
        type "${cmd%% *}" >/dev/null 2>&1 && array_cmd[j]="$cmd,${array_cmd[j]}"
    done
    # append $mimeopener, if defined, otherwise remove trailing comma
    [ -n "$mimeopener" ] && array_cmd[j]="${array_cmd[j]}$mimeopener" || array_cmd[j]="${array_cmd[j]%,}"
    _debug "Command:    ${array_cmd[j]}"
    ((j++))
done
unset array_raw

# without icons, that is all. Otherwise:
if [[ "$useicons" == 1 ]]; then
    type gio >/dev/null || usage "Dependency missing for icons: gio"
    array_icon=()
    _useicons
fi

# make the pipemenu
echo "<openbox_pipe_menu>"
for ((i=0;i<${#array_file[@]};i++)); do
    # label of the menu entry - openbox-specific
    label="${array_file[i]#*$sep}"
    _debug "whole line: ${array_file[i]} - label: $label"
    [[ "$fullpath" == 1 ]] && label="${label/$HOME/\~}" || label="${label##*/}"
    # encode for XML
    # ampersands must be changed first:
    label="${label//&/&amp;}"
    label="${label//\"/&quot;}"
    label="${label//</&lt;}"
    label="${label//>/&gt;}"
    label="${label//_/__}"
    _debug "Label: ${label}"
    echo -n "<menu id=\"$label $i\" label=\"$label\""
    # add icon if applicable
    if [[ "$useicons" == 1 ]]; then
        #~ [ -d "${array_file[i]#*$sep}" ] && 
        _geticon "${array_icon[i]}"
        echo -n " icon=\"$RETVAL\""
    fi
    echo ">" # finalise the opening menu tag
    # command(s) to execute on the file
    oldifs="$IFS"; IFS=","
    cmd=(${array_cmd[i]})
    IFS="$oldifs"
    for c in "${cmd[@]}"; do
        exelabel="Open with $c"
        # escaping single quotes in the file path: stackoverflow.com/a/1315213
        exe="${array_file[i]#*${sep}}"
        exe="${exe//\'/\'\\\'\'}"
        #~ exe="'$c' '$exe'"
        _debug "Execute: $c $exe"
        echo "<item label=\"$exelabel\"><action name=\"Execute\"><command><![CDATA['$0' '-X' \"${array_file[i]%%${sep}*}\" \"$recentfile\" $c '$exe']]></command></action></item>"
    done
    echo "</menu>"
done
_endmenu
